fix(tools): reuse supervisor session for browser_cdp target_id (salvage #32950) - #71743
fix(tools): reuse supervisor session for browser_cdp target_id (salvage #32950)#71743chrisyoung2005 wants to merge 2 commits into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thanks for carrying forward the private-page guard, public snapshot field, and result-redaction fixes from the earlier review.
Problems
- The reported
Target.getTargets→target_idworkflow is still stateless. The new route is conditional on an already-present target ID attools/browser_cdp_tool.py:582;Target.getTargetshas none and falls through to a fresh_cdp_callat:593-596. That does not make the two calls share a Browserless private browser. page_target_ididentifies the supervisor's independently connected browser (tools/browser_supervisor.py:659-690,:770-786), not necessarily the agent-browser daemon's navigated page. The #32685 reproduction documents this split on Browserless, so mixed navigation/CDP usage can silently inspect the wrong page.
Suggested changes
- Route target discovery through the supervisor connection too, then add a per-WebSocket-isolation test for the full discovery/evaluate chain.
- Define and test the mixed daemon/supervisor behavior (verified shared target, fallback, or explicit error) before documenting supervisor state as canonical.
Automated hermes-sweeper review.
| # routing cannot become the sibling bypass for either (the frame_id | ||
| # route follows the same boundary). Falls through to the stateless | ||
| # attach when no live supervisor tracks this target. | ||
| if target_id: |
There was a problem hiding this comment.
Target.getTargets has no target_id, so it bypasses this branch and still reaches the new per-call WebSocket at :593-596. On Browserless that means its returned target belongs to a different private browser; route discovery through the supervisor as well and cover the full discovery-to-evaluate sequence.
0409ed7 to
c7869e1
Compare
|
Both points addressed in Discovery now rides the supervisor connection. Correct — Mixed daemon/supervisor behavior — defined and documented. The semantics this PR now commits to, of your three options, are verified-shared-connection with stateless fallback:
On the second half — |
41db2b6 to
dcdcdbc
Compare
SummaryNine PRs span two distinct causes: #32950 and #71743 address #32685 by reusing a persistent CDP supervisor connection, while #4143, #6623, #27304, #30893, #33246, #34131, and #34497 address execute_code terminal exposure, environment leakage, approval gating, or lost thread context. The execute_code cluster is already consolidated in merged #34497; the remaining open issue is the Browserless per-WebSocket lifecycle handled by the two browser PRs. Related pull requests
Duplicates#4143 and #6623 duplicate the terminal-revocation approach. #27304, #30893, #33246, and #34131 are superseded precursors consolidated in merged #34497; #32950 and #71743 share the browser supervisor-reuse mechanism, with #71743 as the expanded salvage. Suggested consolidationKeep #71743 open with a salvage path: preserve its same-supervisor discovery-to-evaluate chain, guard, redaction, public snapshot field, fallback tests, and attribution, then obtain contributor re-review of the still-visible keep_open verdict. Despite the keep_open review and recorded best-fix status on #32950, close #32950 as duplicate of #71743 because its diff retains the three cited blockers while #71743 carries its core fix and addresses them; no further action is needed for the execute_code PRs because merged best-fix #34497 is their consolidated reference implementation. Complex graphflowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I32685(["issue #32685 (open)"])
subgraph Dup32950 ["PRs duplicating each other"]
P32950["PR #32950 (open)"]
P71743["PR #71743 (open)"]
end
P71743 -->|best fix| I32685
class I32685 open
class P32950 open
class P71743 open
class P32950 best
class P71743 best
class P71743 target
click I32685 "https://github.com/NousResearch/hermes-agent/issues/32685"
click P32950 "https://github.com/NousResearch/hermes-agent/pull/32950"
click P71743 "https://github.com/NousResearch/hermes-agent/pull/71743"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 9 pull requests and 5 issues in this complex. Each diff was read against this issue; Assessment working set: 239 kB of PR diffs, 39 kB of issue/PR text, 23 kB of discussion (25 comments), 38 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
0104e8d to
a666828
Compare
fb1a245 to
10d66a3
Compare
bcec862 to
7af85c1
Compare
|
Thanks — 1 and 4 documented in 1 ( 2 (unlocked 3 (plain-Chrome browser-level behavior change). Confirmed no caller depends on stateless isolation: 4 (partial attach) — comment added at the |
810a51c to
b458538
Compare
b458538 to
7f6eaa6
Compare
|
Status: triage marks this |
45457df to
b68a6b4
Compare
5c62343 to
1f43f9e
Compare
NousResearch#32950) browser_cdp opens a fresh CDP WebSocket per call. On Browserless-style backends that spawn a private browser per connection, a target id from one call is invalid by the next — multi-step Target.getTargets -> target_id workflows always fail (NousResearch#32685). Route target-scoped calls through the live CDPSupervisor session when the target is already attached (top-level page, OOPIF frame, or auto-attached child), falling back to the stateless attach otherwise. Salvages NousResearch#32950 (credit: @LeonSGP43) rebased onto main with the review feedback applied: - the target route runs after the shared private-page guard, so it cannot become the sibling bypass fixed for frame_id in 47764f1 (regression test included) - public target discovery: SupervisorSnapshot now carries page_target_id (surfaced in browser_snapshot output via to_dict); the E2E test uses it instead of private supervisor attributes - the supervisor-backed payload redacts its result via _redact_cdp_output like the stateless payload (regression test included) - browser.md no longer claims every non-frame_id call is stateless
…ection too Review follow-up: Target.getTargets has no target_id, so discovery bypassed the supervisor branch and opened a fresh per-call WebSocket — on Browserless-style backends that enumerates a different private browser than the one target_id-routed calls execute in, leaving the reported discovery -> target_id workflow stateless. _browser_cdp_target_via_supervisor now accepts target_id=None and dispatches browser-level commands (no sessionId) on the supervisor's WebSocket; the call site routes every post-guard call through it when a live supervisor exists. Mixed behavior is now defined and documented: supervisor present -> ALL browser_cdp traffic shares its connection (responses carry connection: "supervisor"); absent, or target unknown to the supervisor -> legacy stateless fallback (plain-Chrome path unchanged). The daemon-vs-supervisor browser split itself is tracked in issue 74216. New tests: browser-level discovery routes via the supervisor (unit, stateless path fails the test if reached), no-supervisor discovery falls back stateless (unit), and a real-Chrome integration test proving the full Target.getTargets -> Runtime.evaluate chain rides one WebSocket. Both supervisor-routing tests fail with the source reverted.
1f43f9e to
8740362
Compare
|
Re-homed onto current PR body rewritten in the Symptom → change → behavior shape. If cherry-picking/salvaging is the faster path for this one, please go ahead — no objection to authorship going either way. |
Fixes #32685. Salvage of #32950 (credit @LeonSGP43 — that branch has been conflicting since May with the review feedback unaddressed; all three review asks are applied here). Triage marked this
best fix — fixesfor the #32685 family.Summary
browser_cdpopens a fresh CDP WebSocket per call. On Browserless-style backends that spawn a private browser per connection, a target id returned by one call is invalid by the next — the documentedTarget.getTargets→target_idworkflow always fails, and even a discovery call enumerates a different browser than the one a follow-up call executes in.CDPSupervisor,browser_cdprides its persistent WebSocket:target_idcalls reuse the supervisor session already attached to that target (top-level page, OOPIF frame, or auto-attached child —CDPSupervisor.resolve_target_session), and browser-level calls (notarget_id, e.g.Target.getTargets) go out on the same connection (_browser_cdp_target_via_supervisor; payloads carry"connection": "supervisor").SupervisorSnapshotgains a publicpage_target_id(surfaced inbrowser_snapshotoutput) so agents never read supervisor internals. No supervisor, or a target the supervisor doesn't track → the legacy stateless attach, unchanged.target_idchain observes ONE browser on per-connection backends; plain Chrome (targets shared across connections) behaves as before. The route runs after the shared_browser_cdp_private_guard(can't become the sibling bypass fixed forframe_idin 47764f1) and the supervisor payload goes through the same_redact_cdp_outputboundary as the stateless one.website/docs/user-guide/features/browser.mdno longer claims every non-frame_idcall is stateless.Evidence
Unit: private-page guard blocks the target route; fallback to stateless without a supervisor; supervisor route redacts a secret result; discovery without
target_idrides the supervisor; discovery without a supervisor falls back. Integration (real Chrome): snapshot exposespage_target_id;target_idroutes via the supervisor; discovery → evaluate rides one connection. New tests fail before the fix (AttributeErroron the missing snapshot field / missing route).Production verification: self-hosted Browserless v2, Fedora 43, rootless podman — chains that previously landed in a fresh empty browser now resolve against the supervisor session (details in the #32685 / #32950 threads). The supervisor↔daemon split for the dialog/snapshot halves is tracked separately in #74216.
Branch state
On current
main— re-homed onto the #102117 module split on 2026-09-04 (both files survived the split; upstream dropped_child_sessions, soresolve_target_sessionchecks the page target then_frames, whose OOPIF frame ids equal their target ids). Happy for this to be cherry-picked / salvaged, or grafted onto #32950 if maintainers prefer the earliest-open PR — either is fine.Platforms tested
Linux (Fedora 43), Python 3.11; Chromium 148 headless (integration) + in-process mock CDP server (unit).
scripts/check-windows-footguns.pyclean on touched files.